﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Xml.Serialization;
using DeepZoomPivotConstructor.DTBuilderLib;
using DeepZoomSLUtilities.HotSpots;

namespace DeepZoomSLUtilities
{
    /// <summary>
    /// Code was loaded from the output of the Composer.
    /// </summary>
    public partial class DeepZoomer : UserControl
    {
        //
        // Based on prior work done by Lutz Gerhard, Peter Blois, and Scott Hanselman
        //

        double zoom = 1;
        bool duringDrag = false;
        bool mouseDown = false;
        Point lastMouseDownPos = new Point();
        Point lastMousePos = new Point();
        Point lastMouseViewPort = new Point();

        #region Dependency Properties
        /// <summary>
        /// Where we go to look for the image source -- hooked up so we can use this as a routed event, etc.
        /// </summary>
        public string Source
        {
            get { return (string)GetValue(SourceProperty); }
            set { SetValue(SourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Source.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty SourceProperty =
            DependencyProperty.Register("Source", typeof(string), typeof(DeepZoomer), new PropertyMetadata((d, e) => ((DeepZoomer)d).UpdateSource()));

        /// <summary>
        /// The source file has changed. Propagate the change.
        /// </summary>
        void UpdateSource()
        {
            msi.Source = new DeepZoomImageTileSource(new Uri(Source, UriKind.Relative));
        }

        #endregion

        public double ZoomFactor
        {
            get { return zoom; }
            set { zoom = value; }
        }

        public DeepZoomer()
        {
            InitializeComponent();

            //
            // Firing an event when the MultiScaleImage is Loaded
            //
            this.msi.Loaded += new RoutedEventHandler(msi_Loaded);

            //
            // Firing an event when all of the images have been Loaded
            //
            this.msi.ImageOpenSucceeded += new RoutedEventHandler(msi_ImageOpenSucceeded);

            ///
            /// Cache this control for later use by the hot spot stuff
            /// 

            HotSpotInfo.ZoomControl = this;

            ///
            /// Deal with mouse clicking, etc.
            /// 

            this.MouseMove += new MouseEventHandler(DeepZoomer_MouseMove);
            this.MouseLeftButtonDown += new MouseButtonEventHandler(DeepZoomer_MouseLeftButtonDown);

            ///
            /// And the wheel for changing the zoom level.
            /// 

            new MouseWheelHelper(this).Moved += delegate(object sender, MouseWheelEventArgs e)
            {
                e.Handled = true;
                ZoomByFactor(e.Delta < 0, 1.3, msi.ElementToLogicalPoint(this.lastMousePos));
                msi.CaptureMouse();
            };
        }

        /// <summary>
        /// Mouse button down... Are we about to do a drag??
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void DeepZoomer_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            lastMouseDownPos = e.GetPosition(msi);
            lastMouseViewPort = msi.ViewportOrigin;

            mouseDown = true;

            msi.CaptureMouse();
        }

        /// <summary>
        /// The mouse is being moved. Track any changes.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void DeepZoomer_MouseMove(object sender, MouseEventArgs e)
        {
            lastMousePos = e.GetPosition(msi);

            ///
            /// Start a drag - the mouse is moving while the mouse is down
            /// 

            if (mouseDown && !duringDrag)
            {
                duringDrag = true;
                double w = msi.ViewportWidth;
                Point o = new Point(msi.ViewportOrigin.X, msi.ViewportOrigin.Y);
                msi.UseSprings = false;
                msi.ViewportOrigin = new Point(o.X, o.Y);
                msi.ViewportWidth = w;
                zoom = 1 / w;
                msi.UseSprings = true;
                msi.Cursor = Cursors.Arrow;
            }

            ///
            /// Since we are now dragging, we need to pan around the image.
            /// 

            if (duringDrag)
            {
                Point newPoint = lastMouseViewPort;
                newPoint.X += (lastMouseDownPos.X - lastMousePos.X) / msi.ActualWidth * msi.ViewportWidth;
                newPoint.Y += (lastMouseDownPos.Y - lastMousePos.Y) / msi.ActualWidth * msi.ViewportWidth;
                msi.ViewportOrigin = newPoint;
            }

            ///
            /// Not during a drag or anything else like that, so... see if we are over a hot-spot that will do somethign when we
            /// click.
            /// 

            if (!duringDrag & !mouseDown)
            {
                var hs = FindActiveHS(msi.ElementToLogicalPoint(e.GetPosition(msi)));
                if (hs != null)
                {
                    msi.Cursor = Cursors.Hand;
                }
                else
                {
                    msi.Cursor = Cursors.Arrow;
                }
            }
        }

        /// <summary>
        /// The left button came up. Depending on what was going on we either:
        ///     - finish a drag
        ///     - zoom in a bit
        ///     - Click on a hotspot
        /// </summary>
        /// <param name="e"></param>
        protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
        {
            ///
            /// Not during a drag
            ///   - zoom in a bit
            ///   - click on a hotspot
            ///   

            if (!duringDrag && hotSpotHelpers != null)
            {
                Point msiPoint = msi.ElementToLogicalPoint(lastMousePos);

                HotSpotInfo hsFound = FindActiveHS(msiPoint);
                if (hsFound != null)
                {
                    hsFound.Execute(HotSpotInfo.MouseButtons.LeftButton);
                }
                else
                {
                    ///
                    /// No hot spot - simple zoom
                    /// 

                    bool shiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
                    ZoomByFactor(shiftDown, 2.0, msiPoint);
                }
            }

            ///
            /// Reset if we were during a drag - it is safe to do this stuff no matter what.
            /// 

            duringDrag = false;
            mouseDown = false;

            msi.ReleaseMouseCapture();
        }

        /// <summary>
        /// Find the active HS info given a point in logical coordinates (in hsort, the msi.ElementToLogicalPoint has been called).
        /// </summary>
        /// <param name="msiPoint"></param>
        /// <returns></returns>
        private HotSpotInfo FindActiveHS(Point msiPoint)
        {
            if (msiPoint == null || hotSpotHelpers == null)
            {
                return null;
            }

            var activeHotSpots = from subimageHS in hotSpotHelpers
                                 where subimageHS != null && subimageHS.Contains(msiPoint)
                                 let localPoint = subimageHS.ConvertSubimageToLocal(msiPoint)
                                 from hs in subimageHS.FindActiveHotspots(localPoint)
                                 where hs.ActivatedByMouseClick(HotSpotInfo.MouseButtons.LeftButton)
                                 orderby hs.ZIndex descending
                                 select hs;

            HotSpotInfo hsFound = activeHotSpots.FirstOrDefault();
            return hsFound;
        }

        /// <summary>
        /// Easy lookup to figure out how much to move the display when a up/down/left/right
        /// arrow key is collected. These are fractional numbers - fraction of the displayed.
        /// </summary>
        private Dictionary<DZHotSpotMetaData.ActionData.KeyPressed, Point> KeyMovementAmount = new Dictionary<DZHotSpotMetaData.ActionData.KeyPressed, Point>()
        {
            {DZHotSpotMetaData.ActionData.KeyPressed.ArrowDown, new Point (0.0, 0.10)},
            {DZHotSpotMetaData.ActionData.KeyPressed.ArrowUp, new Point (0.0, -0.10)},
            {DZHotSpotMetaData.ActionData.KeyPressed.ArrowRight, new Point (0.10, 0.0)},
            {DZHotSpotMetaData.ActionData.KeyPressed.ArrowLeft, new Point (-0.10, 0.0)}
        };

        /// <summary>
        /// User has pressed a key... see if it is one we know
        /// how to handle!
        /// </summary>
        /// <param name="e"></param>
        protected override void OnKeyUp(KeyEventArgs e)
        {
            DZHotSpotMetaData.ActionData.KeyPressed aKey = ConvertToDZKey(e.Key);
            if (aKey != DZHotSpotMetaData.ActionData.KeyPressed.NoKey)
            {
                ///
                /// Use the center point as what we are going to look
                /// at for a hot spot
                /// 

                Point msiPoint = GetCenterInLogicalCoordinates();

                var activeHotSpots = from subimageHS in hotSpotHelpers
                                     where subimageHS.Contains(msiPoint)
                                     let localPoint = subimageHS.ConvertSubimageToLocal(msiPoint)
                                     from hs in subimageHS.FindActiveHotspots(localPoint)
                                     where hs.ActivatedByKeyPress(aKey)
                                     orderby hs.ZIndex descending
                                     select hs;

                HotSpotInfo hsFound = activeHotSpots.FirstOrDefault();

                if (hsFound != null)
                {
                    hsFound.Execute(aKey);
                    e.Handled = true;
                    return;
                }
            }

            ///
            /// Move the display one direction or the other
            /// 

            if (KeyMovementAmount.ContainsKey(aKey))
            {
                Point deltaFraction = KeyMovementAmount[aKey];
                Point pos = new Point(
                    msi.ViewportOrigin.X + deltaFraction.X * msi.ViewportWidth,
                    msi.ViewportOrigin.Y + deltaFraction.Y * msi.ViewportWidth * msi.ActualHeight / msi.ActualWidth);

                PanToOrigin(pos);

                e.Handled = true;
                return;
            }

            //
            // If the "r" or "R" key is hit, then we toggle the random mode.
            //

            if (e.Key == Key.R)
            {
                ToggleRandomMode();
            }

            ///
            /// Drop through here if nothing is working right!
            /// 

            base.OnKeyUp(e);
        }

        /// <summary>
        /// What is the random mode in?
        /// </summary>
        private bool _randomMode = false;

        /// <summary>
        /// Hold onto the timer.
        /// </summary>
        private Timer _randomModeTimer = null;

        /// <summary>
        /// The random number generator we use to go after things.
        /// </summary>
        private Random _randomModeGenerator = null;

        /// <summary>
        /// How many goes shoudl we have before we return to zero?
        /// </summary>
        private int _randomModeReturnToHome = 0;

        /// <summary>
        /// Number of seconds between new slides.
        /// </summary>
        private int _randomModeInterval = 10;

        /// <summary>
        /// Turn random mode on or off!
        /// </summary>
        private void ToggleRandomMode()
        {
            // If we aren't ready, out!
            if (hotSpotHelpers == null)
            {
                return;
            }

            // Move to home from whatever we are looking at, no matter if we are turning on or off.
            DisplayHomeView();

            // Next see what we are doing.
            _randomMode = !_randomMode;

            // And get things going, or shut it down.
            if (_randomMode)
            {
                if (_randomModeGenerator == null)
                    _randomModeGenerator = new Random();
                ResetReturnToHomeCount();
                _randomModeTimer = new Timer(o => NextRandomModeImage(), null, 2 * 1000, _randomModeInterval * 1000);
            }
            else
            {
                _randomModeTimer.Dispose();
                _randomModeTimer = null;
            }
        }

        /// <summary>
        /// Calculate how  many goes before our random guy returns to home.
        /// </summary>
        private void ResetReturnToHomeCount()
        {
            var interval = 3 * 60 / (double)_randomModeInterval;
            _randomModeReturnToHome = (int)interval;
        }

        /// <summary>
        /// Marked if an update is on gonig.
        /// </summary>
        private bool _randomUpdateInProgress = false;

        /// <summary>
        /// Move to the next random mode image.
        /// </summary>
        /// <returns></returns>
        /// <remarks>
        /// Uses total number of hot spots and then throws a random number.
        /// </remarks>
        private void NextRandomModeImage()
        {
            if (!_randomUpdateInProgress)
            {
                _randomUpdateInProgress = true;
                try
                {
                    if (_randomModeReturnToHome == 0)
                    {
                        Dispatcher.BeginInvoke(() => DisplayHomeView());
                        ResetReturnToHomeCount();
                    }
                    else
                    {
                        _randomModeReturnToHome--;

                        var hsGood = hotSpotHelpers.Where(h => h != null);
                        var hsCount = hsGood.Select(h => h.NumberCenteringHS()).ToArray();
                        var hsTotal = hsCount.Sum();

                        var hsZoomTo = _randomModeGenerator.Next(hsTotal);
                        int index = 0;
                        while (hsZoomTo > 0)
                        {
                            hsZoomTo -= hsCount[index];
                            index++;
                        }
                        index--;

                        var hs = hsGood.Skip(index).FirstOrDefault().GetNthCenteringHS(hsZoomTo + hsCount[index]);
                        if (hs != null)
                            Dispatcher.BeginInvoke(() => hs.Execute(HotSpotInfo.MouseButtons.LeftButton));
                    }
                }
                finally
                {
                    _randomUpdateInProgress = false;
                }
            }
        }

        /// <summary>
        /// Convert a key-key to a DZ key - the internal representation.
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        private DZHotSpotMetaData.ActionData.KeyPressed ConvertToDZKey(Key key)
        {
            if (key == Key.Right)
            {
                return DZHotSpotMetaData.ActionData.KeyPressed.ArrowRight;
            }
            if (key == Key.Left)
            {
                return DZHotSpotMetaData.ActionData.KeyPressed.ArrowLeft;
            }
            if (key == Key.Up)
            {
                return DZHotSpotMetaData.ActionData.KeyPressed.ArrowUp;
            }
            if (key == Key.Down)
            {
                return DZHotSpotMetaData.ActionData.KeyPressed.ArrowDown;
            }
            return DZHotSpotMetaData.ActionData.KeyPressed.NoKey;
        }

        /// <summary>
        /// Zoom in or out by some factor
        /// </summary>
        /// <param name="zoomOut">If true then we zoom out. Otherwise zoom in</param>
        private void ZoomByFactor(bool zoomOut, double factor, Point lastM)
        {
            double newzoom = zoom;


            if (zoomOut)
            {
                newzoom /= factor;
            }
            else
            {
                newzoom *= factor;
            }

            Zoom(newzoom, lastM);
        }

        /// <summary>
        /// Relative URI to use when trying to download anything
        /// </summary>
        private string RelativeURI;

        /// <summary>
        /// The MSI collection has shown up. Load up the web file that contains our meta data
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void msi_ImageOpenSucceeded(object sender, RoutedEventArgs e)
        {
            RelativeURI = Source.Substring(0, Source.LastIndexOf("/"));

            Uri u = new Uri(RelativeURI + "/meetingMetadata.xml", UriKind.Relative);
            WCFactory().DownloadStringAsync(u);
        }

        /// <summary>
        /// Create a web client ready for use!
        /// </summary>
        /// <returns></returns>
        private WebClient WCFactory()
        {
            WebClient c = new WebClient();
            c.BaseAddress = c.BaseAddress.Substring(0, c.BaseAddress.LastIndexOf("/") + 1);
            c.DownloadStringCompleted += new DownloadStringCompletedEventHandler(c_DownloadStringCompleted);
            return c;
        }

        /// <summary>
        /// The image meta data for the image we are looking at.
        /// </summary>
        private DZTalkVisualMetaData[] _imageMetaData;

        /// <summary>
        /// Called when we have a complete download of the main image specification xml file.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        void c_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            if (e.Error != null)
            {
                ///
                /// Things have gone very wrong. We should get out of here!
                /// 

                return;
            }

            ///
            /// Ok - check out if this is image meta-data or hot spot info, and then do
            /// the "right thing", as they say
            /// 

            if (e.UserState == null)
            {
                ExtractImageMetaData(e);
            }
            else
            {
                ExtractHotSpotData(e);
            }

        }

        /// <summary>
        /// Helper objects that allow us to parse hot spot info and
        /// figure out if one is being used/clicked by the user.
        /// </summary>
        private SubImageHotSpotInfo[] hotSpotHelpers = null;

        private class HotSpotQueryInfo
        {
            public int HSIndex { get; set; }
            public XmlSerializer HSCommonSerializer { get; set; }
        }

        /// <summary>
        /// We've just gotten some new hot spot information. Extract it
        /// and store it!
        /// </summary>
        /// <param name="e"></param>
        private void ExtractHotSpotData(DownloadStringCompletedEventArgs e)
        {
            HotSpotQueryInfo queryInfo = e.UserState as HotSpotQueryInfo;

            if (queryInfo.HSIndex >= hotSpotHelpers.Length)
            {
                return;
            }

            StringReader r = new StringReader(e.Result);
            DZHotSpotMetaData[] hsdata = queryInfo.HSCommonSerializer.Deserialize(r) as DZHotSpotMetaData[];

            hotSpotHelpers[queryInfo.HSIndex] = new SubImageHotSpotInfo(_imageMetaData[queryInfo.HSIndex], hsdata);
        }

        /// <summary>
        /// We've just finished loading teh main image meta-data. Parse it
        /// and get ourselves all set up!
        /// </summary>
        /// <param name="e"></param>
        private void ExtractImageMetaData(DownloadStringCompletedEventArgs e)
        {
            ///
            /// Deseralize the information into something we can deal with
            /// 

            XmlSerializer ser = new XmlSerializer(typeof(DZTalkVisualMetaData[]));
            StringReader r = new StringReader(e.Result);
            _imageMetaData = ser.Deserialize(r) as DZTalkVisualMetaData[];

            ///
            /// Now, update the collection with the information that is in the image meta data.
            /// 

            if (_imageMetaData.Length != msi.SubImages.Count)
            {
                return;
            }

            int i = 0;
            foreach (var subImage in msi.SubImages)
            {
                DZTalkVisualMetaData md = _imageMetaData[i];
                subImage.ViewportWidth = 1.0 / md.Width;
                subImage.ViewportOrigin = new Point(-md.X / md.Width, -md.Y / md.Width);
                subImage.ZIndex = md.Z;
                subImage.Opacity = 1;
                i = i + 1;
            }

            msi.ViewportWidth = 1;

            ///
            /// Queue up the hot spot information web requests!
            /// 

            hotSpotHelpers = new SubImageHotSpotInfo[msi.SubImages.Count];
            i = 0;
            XmlSerializer hsSer = new XmlSerializer(typeof(DZHotSpotMetaData[]));
            foreach (var md in _imageMetaData)
            {
                Uri u = new Uri(RelativeURI + "/" + md.RelativeName.Replace('\\', '/') + "-HotSpotdata.xml", UriKind.Relative);
                HotSpotQueryInfo q = new HotSpotQueryInfo() { HSIndex = i, HSCommonSerializer = hsSer };
                WCFactory().DownloadStringAsync(u, q);
                i = i + 1;
            }

        }

        void msi_Loaded(object sender, RoutedEventArgs e)
        {
            // Hook up any events you want when the image has successfully been opened
        }

        private void Zoom(double newzoom, Point p)
        {
            if (newzoom < 0.5)
            {
                newzoom = 0.5;
            }

            msi.ZoomAboutLogicalPoint(newzoom / zoom, p.X, p.Y);
            zoom = newzoom;
        }

        /// <summary>
        /// Like zoom above, but also make sure the p is centered when we are done.
        /// </summary>
        /// <param name="newzoom"></param>
        /// <param name="p"></param>
        private void ZoomAndPan(double newzoom, Point p)
        {
            Zoom(newzoom, p);
            double halfWidth = 1.0 / zoom / 2.0;
            double halfHeight = halfWidth * msi.ActualHeight / msi.ActualWidth;
            msi.ViewportOrigin = new Point(p.X - halfWidth, p.Y - halfHeight);
        }

        /// <summary>
        /// Move the origin point from where it is to where we'd like it to be.
        /// </summary>
        /// <param name="pos"></param>
        private void PanToOrigin(Point pos)
        {
            msi.ViewportOrigin = pos;
        }


        private void ZoomInClick(object sender, System.Windows.RoutedEventArgs e)
        {
            Zoom(zoom * 1.3, GetCenterInLogicalCoordinates());
        }

        private Point GetCenterInLogicalCoordinates()
        {
            return msi.ElementToLogicalPoint(new Point(.5 * msi.ActualWidth, .5 * msi.ActualHeight));
        }

        private void ZoomOutClick(object sender, System.Windows.RoutedEventArgs e)
        {
            Zoom(zoom / 1.3, msi.ElementToLogicalPoint(new Point(.5 * msi.ActualWidth, .5 * msi.ActualHeight)));
        }

        private void DisplayHomeView()
        {
            this.msi.ViewportWidth = 1;
            this.msi.ViewportOrigin = new Point(0, 0);
            ZoomFactor = 1;
        }

        private void GoHomeClick(object sender, System.Windows.RoutedEventArgs e)
        {
            DisplayHomeView();
        }

        private void GoFullScreenClick(object sender, System.Windows.RoutedEventArgs e)
        {
            if (!Application.Current.Host.Content.IsFullScreen)
            {
                Application.Current.Host.Content.IsFullScreen = true;
            }
            else
            {
                Application.Current.Host.Content.IsFullScreen = false;
            }
        }

        // Handling the VSM states
        private void LeaveMovie(object sender, System.Windows.Input.MouseEventArgs e)
        {
            VisualStateManager.GoToState(this, "FadeOut", true);
        }

        private void EnterMovie(object sender, System.Windows.Input.MouseEventArgs e)
        {
            VisualStateManager.GoToState(this, "FadeIn", true);
        }


        // unused functions that show the inner math of Deep Zoom
        public Rect getImageRect()
        {
            return new Rect(-msi.ViewportOrigin.X / msi.ViewportWidth, -msi.ViewportOrigin.Y / msi.ViewportWidth, 1 / msi.ViewportWidth, 1 / msi.ViewportWidth * msi.AspectRatio);
        }

        public Rect ZoomAboutPoint(Rect img, double zAmount, Point pt)
        {
            return new Rect(pt.X + (img.X - pt.X) / zAmount, pt.Y + (img.Y - pt.Y) / zAmount, img.Width / zAmount, img.Height / zAmount);
        }

        /// <summary>
        /// Simplistic make a rect visible - doesn't quite get it right...
        /// </summary>
        /// <param name="rect">Rect to make visible in the default DZI parameters</param>
        public void LayoutDZI(Rect rect)
        {
            //msi.ViewportWidth = 1 / rect.Width;
            //msi.ViewportOrigin = new Point(-rect.Left / rect.Width, -rect.Top / rect.Width);
            msi.ViewportWidth = 1 / rect.Width;
            msi.ViewportOrigin = new Point(-rect.Left / rect.Width, -rect.Top / rect.Width);
        }

        /// <summary>
        /// Given a rect in our standard global coordinates, make sure it is visible and centered.
        /// </summary>
        /// <param name="rect"></param>
        public void CenterRectVisible(Rect rect)
        {
            double wZoom = 1.0 / rect.Width;

            double pixelScale = msi.ActualHeight / rect.Height;
            double pixelWidth = pixelScale * rect.Width;
            double pixelWidthFraction = pixelWidth / msi.ActualWidth;
            double hZoom = wZoom * pixelWidthFraction;

            double zoom = wZoom < hZoom ? wZoom : hZoom;

            Point center = new Point(rect.X + rect.Width / 2.0, rect.Y + rect.Height / 2.0);

            ZoomAndPan(zoom, center);
        }
    }

}
